Break api into 2 parts#36
Conversation
|
Hi @Steffen911 Please see my initial implementation of the separation. Its a lot and I'm happy to iterate on it. The biggest thing I tried to achieve is to keep as much build-time auto-generation as possible. Fern itself is not very customizeable, so I started from an OpenAPI document, which can be generated from fern. There is very little code actually checked into version control - most of the power comes from the fact it is generated at build time. Happy to answer any questions you may have. You might think that 491 file changes is a lot, but more than half of that is simply moving what was there (generated by fern) into a subdirectory so it can be preserved. |
…guration options, and testing setup with Testcontainers Add integration tests for all major APIs, including async and legacy modes Refactor configuration handling and improve ingestion-related tests Add async tests for all major APIs Enable Bean Validation support in `langfuse-java-api`. Define module-info files for `langfuse-java-api` and `langfuse-java-client`. Tests Adding request/response logging Adding testcontainers Adding additional apis Initial separation
…ames for improved clarity.
…LangfuseAsyncApis`).
|
Thought I would mention this too - there are Untyped Map-Like Fields in the Langfuse OpenAPI Spec. I'll open a new issue for this too. ProblemThe Langfuse OpenAPI spec ( In the OpenAPI 3.0 specification, a property with no The spec already correctly types the same fields in other schemas (see Correctly Typed Fields below), making this an internal inconsistency rather than a design choice. How We Identified Which Fields Should Be TypedWe used four criteria to determine which untyped fields should have 1. Internal Consistency Within the SpecThe same field name is correctly typed in some schemas but untyped in others. For example:
If the field is typed as 2. Langfuse DocumentationThe Langfuse documentation explicitly describes these fields as object/dictionary types:
3. Semantic AnalysisSome fields are structurally always JSON objects by nature:
4. Exclusion Criteria — Fields That Should Remain UntypedWe explicitly excluded fields that can legitimately be any JSON value:
Correctly Typed Fields (8)These fields already have proper
Fields Missing Type Definitions (28)These fields should have
|
| Schema | Current Definition | Nullable |
|---|---|---|
Trace |
no type specified | true |
Observation |
no type specified | not set |
ObservationV2 |
no type specified | true |
ObservationBody |
no type specified | true |
OptionalObservationBody |
no type specified | true |
BaseScore |
no type specified | not set |
BaseScoreV1 |
no type specified | not set |
ScoreBody |
no type specified | true |
Dataset |
no type specified | not set |
DatasetItem |
no type specified | not set |
DatasetRun |
no type specified | not set |
CreateDatasetItemRequest |
no type specified | true |
CreateDatasetRunItemRequest |
no type specified | true |
CreateDatasetRequest |
no type specified | true |
TraceBody |
no type specified | true |
BaseEvent |
no type specified | true |
modelParameters (2 fields)
| Schema | Current Definition | Nullable |
|---|---|---|
Observation |
no type specified | not set |
ObservationV2 |
no type specified | true |
Note: The write-side schemas (CreateGenerationBody, UpdateGenerationBody, ObservationBody) already correctly type modelParameters as type: object with additionalProperties: { $ref: MapValue }.
config (3 fields)
| Schema | Current Definition | Nullable |
|---|---|---|
BasePrompt |
empty definition ({}) |
N/A |
CreateChatPromptRequest |
no type specified | true |
CreateTextPromptRequest |
no type specified | true |
Note: LlmConnection.config and UpsertLlmConnectionRequest.config already correctly use type: object with additionalProperties: true.
tokenizerConfig (2 fields)
| Schema | Current Definition | Nullable |
|---|---|---|
Model |
no type specified | not set |
CreateModelRequest |
no type specified | true |
inputSchema / expectedOutputSchema (4 fields)
| Schema | Field | Current Definition | Nullable |
|---|---|---|---|
Dataset |
inputSchema |
no type specified | true |
Dataset |
expectedOutputSchema |
no type specified | true |
CreateDatasetRequest |
inputSchema |
no type specified | true |
CreateDatasetRequest |
expectedOutputSchema |
no type specified | true |
lastConfig (1 field)
| Schema | Current Definition | Nullable |
|---|---|---|
PromptMeta |
no type specified | not set |
Cascading Impact via Schema Composition (30 schemas)
The 28 untyped fields cascade into 30 additional composed schemas via oneOf, allOf, and anyOf. Fixing the base schemas will automatically fix all of these:
| Composed Schema | Inherits From | Untyped Field |
|---|---|---|
TraceWithDetails |
Trace |
metadata |
TraceWithFullDetails |
Trace |
metadata |
ObservationsView |
Observation |
metadata, modelParameters |
BooleanScore |
BaseScore |
metadata |
CategoricalScore |
BaseScore |
metadata |
CorrectionScore |
BaseScore |
metadata |
NumericScore |
BaseScore |
metadata |
TextScore |
BaseScore |
metadata |
BooleanScoreV1 |
BaseScoreV1 |
metadata |
CategoricalScoreV1 |
BaseScoreV1 |
metadata |
NumericScoreV1 |
BaseScoreV1 |
metadata |
TextScoreV1 |
BaseScoreV1 |
metadata |
DatasetRunWithItems |
DatasetRun |
metadata |
ChatPrompt |
BasePrompt |
config |
TextPrompt |
BasePrompt |
config |
CreatePromptRequest |
CreateChatPromptRequest / CreateTextPromptRequest |
config |
CreateEventBody |
OptionalObservationBody |
metadata |
UpdateEventBody |
OptionalObservationBody |
metadata |
CreateEventEvent |
BaseEvent |
metadata |
CreateGenerationEvent |
BaseEvent |
metadata |
CreateObservationEvent |
BaseEvent |
metadata |
CreateSpanEvent |
BaseEvent |
metadata |
UpdateGenerationEvent |
BaseEvent |
metadata |
UpdateObservationEvent |
BaseEvent |
metadata |
UpdateSpanEvent |
BaseEvent |
metadata |
SDKLogEvent |
BaseEvent |
metadata |
ScoreEvent |
BaseEvent |
metadata |
TraceEvent |
BaseEvent |
metadata |
Fields Correctly Left Untyped (18)
These fields are intentionally untyped because they can be any JSON value (string, number, array, object, or null). No changes needed:
| Schema | Field | Reason |
|---|---|---|
Trace |
input, output |
Documented as "Can be any JSON" |
Observation |
input, output |
Documented as "Can be any JSON" |
ObservationV2 |
input, output |
Documented as "Can be any JSON" |
ObservationBody |
input, output |
Documented as "Can be any JSON" |
OptionalObservationBody |
input, output |
Documented as "Can be any JSON" |
TraceBody |
input, output |
Documented as "Can be any JSON" |
DatasetItem |
input, expectedOutput |
Documented as "Can be any JSON" |
CreateDatasetItemRequest |
input, expectedOutput |
Documented as "Can be any JSON" |
IngestionError |
error |
Error payloads can be any structure |
SDKLogBody |
log |
SDK debug payload, any JSON |
Suggested Fix
For each of the 28 fields listed above, add type: object and additionalProperties: true to the property definition. For example, change:
metadata:
nullable: true
description: Additional metadata of the observationto:
metadata:
type: object
additionalProperties: true
nullable: true
description: Additional metadata of the observationAnd for the empty-definition case (BasePrompt.config: {}), change to:
config:
type: object
additionalProperties: true
IMHO, as you are already touching this, you should introduce an HTTP abstraction as done in LangChain4j which allows for the different HTTP clients. |
|
So theoretically I would be able to plug in a Jakarta REST Client? |
That's exactly what's being done here (and was the whole purpose of this PR). There are now 2 modules - an API and a client. They're completely decoupled. |
See the quarkus-langfuse extension. That's exactly what it does. For now the quarkus-langfuse extension has a copy of what this pr is doing, but that will get swapped out once this is merged and released |
|
Wonderful! |
Summary
Separates the Langfuse Java SDK into a multi-module Maven project with generated API interfaces, a reference HTTP client, and Testcontainers support for integration testing.
It is mostly dependency-free. It does not use/require OkHttp, instead using Java's built-in
HttpClient. It requires either Jackson 2 or 3, but does not force a user into one over the other, nor does it force which version to use.The API has been completely decoupled to the underlying http transport, meaning the api can be re-used with other http transports.
New modules
build time and never checked into version control.
java.net.http.HttpClientwith dual Jackson 2/3 support, request/response logging with sensitive header masking, and automatic HTTP/1.1 fallback for plain HTTP connections.LangfuseContainerthat orchestrates a full Langfuse environment (PostgreSQL, ClickHouse, Redis, MinIO, web server, worker) for testing via Testcontainers.Key design decisions
LangfuseApi.builder()usesServiceLoaderto find the client implementation, allowing framework-specific implementations (Spring, Quarkus) without depending on the reference client.useSingleRequestParameter), eliminating long parameter lists (e.g.scoresGetManyhad 21 parameters).protected; all creation goes throughbuilder().@JsonInclude(NON_EMPTY)at the class level so empty optional containers are omitted from serialized JSON.@NotNull,@Size, etc. annotations generated from OpenAPI schema constraints.com.fasterxml.jacksonandtools.jacksonannotations. The client auto-detects which Jackson version is on the classpath at runtime.CompletionStage-based async variant.Testcontainers
Startables.deepStartgetAllLogs()returns aMap<String, String>of all container logs for diagnosticsdocker-compose.ymlOptional<Duration>for ingestion queue delay and ClickHouse write interval settingsTest coverage
tests (sync + async) across 19 API areas, all running against a real Langfuse environment via Testcontainers:
Health, Ingestion, Traces, Prompts, Prompt Versions, Scores, Score Configs, Datasets, Dataset Items, Dataset Run Items, Models, Projects, Observations, Sessions, Comments, Annotation Queues, plus LegacyScoreV1 and LegacyObservationsV1 used as helpers.
Other changes
module-info.javafor API and client moduleslogRequests(),logResponses(),prettyPrint()builder optionsApiClientfor plainhttpURLs (avoids HTTP/2 upgrade issues)com.langfuse.testcontainers.service) on all Docker containersCloses #31